#!/usr/bin/ruby

###################################################
#        MFdhcp.rb                                                                                                            
#        description;                                                                                                          
#            provide dhcp control functions for MFctrl                                                 
#        version           1.7                                                                                         
#        last modified     07/07/06
#       Last Change,allow ping-test and make sure dhcp isn't running prior to starting
###################################################

DHCP_VERSION=1.7
def dhcp_ver
   return "\tDHCP library version: #{DHCP_VERSION}\n"
end
def dhcp_stop
    if !active?("/jail/dhcp")
        return "\tSKIPPING: DHCP deactivated in global config\n"
    end
    tmp=Array.new
    retary=Array.new
    retary << "STOPING DHCPD service\n"
    tmp=`#{$ps} ax | #{$grep} dhcpd | #{$grep} -v grep`
    tmp.each {|line|
        t=line.split(" ")
        retary << `#{$kill} -9 #{t[0]}`
    }
    if !retary[1]
        retary << "\tDHCPD service not running\n"
    end
    return retary
end

def dhcp_start
    # if dhcpd is already running we need to stop it
    ignore=dhcp_stop()
    # check to be sure dhcp is active
    if !active?("/jail/dhcp")
        return "\tSKIPPING: DHCP deactivated in global config\n"
    end
    if !$dhcpd
        return "ABORTING DHCPD (no dhcpd service found)\n"
    end
    
    ints=Array.new
    retary=Array.new
    gcfg,dl,ml,fw,active,bc,rg,rt=parse_confopts(MFreadlines("/jail/dhcp/options"))
    if !dl
        gcfg << "default-lease-time 14400;"
    end
    if !ml
        gcfg << "max-lease-time 28000;"
    end
    # ok, we are active and we have our defaults, go ahead and write the dhcpd.conf header files
    dhout=File.open("/etc/dhcp3/dhcpd.conf","w")
    
    dhout.puts "# DYNAMICLY GENERATED, ALL CHANGES WILL BE OVERWRITTEN"
    dhout.puts "ddns-update-style interim;\n" # FIGURE OUT A BETTER WAY
    dhout.puts "ignore client-updates;\n"
    gcfg.each {|line|
        dhout.puts line
    }
    
    # ok we have our header, do our per zone stuff
    zones=Dir.new("/jail/acl/zones")
    zones.each {|z|    
            next if z !~ /[0-9a-zA-Z]/
            next if z == "GLOBAL"
            next if z == "WAN"
            mycfg=Array.new
            mycfg,mdl,mml,mfw,mactive,mbc,mrg,mrt=parse_confopts(MFreadlines("/jail/acl/zones/#{z}/interface/dhcp"))
            next if !mycfg || mactive.downcase == "no"
            itmp=MFreadlines("/jail/acl/zones/#{z}/interface/int")
            next if !itmp
            myint=itmp.shift
            iptmp=MFreadlines("/jail/acl/zones/#{z}/interface/ip")
            next if !iptmp
            ip=iptmp.shift
            next if !is_ip(ip)
            nmtmp=MFreadlines("/jail/acl/zones/#{z}/interface/netmask")
            next if !nmtmp
            nm=nmtmp.shift
            next if !is_ip(nm)
            # ok so we have a zone with a valid dhcp file, and interface info, so lets set the options and write it out
            netid=find_network(ip,nm)
            retary << "\tActivating DHCP config for zone: #{z}\n"
            if !mbc
                mbc=find_broadcast(ip,nm)
                mycfg << "option broadcast-address #{mbc};"
            end
            if mrg
                # ok we have a range, lets make sure it is valid
                tmp=mrg.split(" ")
                require 'ipaddr'
                test_net=IPAddr.new("#{ip}/#{nm}")
                tmp.each{|x|
                    if ! test_net.include?(IPAddr.new(x))
                        # ok if we don't have a valid range, then drop it and build our own
                        retary << "RANGE #{mrg} is not vald for zone #{z}\nAdding a default range, please verify your configuration\n"
                        mrg=nil
                    end
                }
            end
            if ! mrg
                # ok, we don't have a range, but we are going to be doing dhcp... take our local subnet and figure it out
                btmp=ip.split("\.")
                ipnum=btmp[3].to_i
                base="#{btmp[0]}.#{btmp[1]}.#{btmp[2]}."
                btmp2=mbc.split("\.")
                top=btmp2[3].to_i-1
                btmp3=find_network(ip,nm).split("\.")
                bottom=btmp3[3].to_i+1
                if ipnum == bottom
                    # fw is ip'ed with the first usable ip, so add one and make the rest a range
                    mycfg << "range #{base}#{ipnum+1} #{base}#{top};"
                elsif ipnum == top
                    # fw is ip'ed with the last usable ip, so go from bottom to ip -1
                    mycfg << "range #{base}#{bottom} #{base}#{ipnum-1};"
                else     # we are somewhere in the middle, so make two ranges
                    mycfg << "range #{base}#{bottom} #{base}#{ipnum-1};"
                    mycfg << "range #{base}#{ipnum+1} #{base}#{top};"
                end
            end

            if !mrt
                mycfg << "option routers #{ip};"
            end
            # ok, so we have an active option, write it to the dhcpd.conf
            # start with the header stuff, we determine
            dhout.puts "\n# zone #{z}"
            dhout.puts "subnet #{netid} netmask #{nm} {\n"
            mycfg.each {|line|
                dhout.puts "\t#{line}"
            }            
            dhout.puts "}"
            
            # ok, now we have our conf, lets see if we have a dhcp.hosts dir 
            if test(?d, "/jail/acl/zones/#{z}/interface/dhcp.hosts")
                dh=Dir.new("/jail/acl/zones/#{z}/interface/dhcp.hosts")
                dh.each {|h|
                    next if h !~ /[0-9a-zA-Z]/
                    tmp=MFreadlines("/jail/acl/zones/#{z}/interface/dhcp.hosts/#{h}")
                    if tmp
                       myhost_line = Array.new
                       i_am_active = "yes"
                        tmp.each {|l|
                            hk,hv=l.split("=")
                            hkey=hk.downcase
                            case hkey
                                when "active";
                                    if hv.downcase == "no"
                                        i_am_active = "no"
                                    end
                                when "mac"; myhost_line << "\thardware ethernet #{hv};"
                                when "fixed-address";
                                    if is_ip(hv) || is_fqdn(hv)
                                        myhost_line << "\tfixed-address #{hv};"
                                    end
                                when "filename"; myhost_line << "\tfilename \"#{hv.gsub('\"','')}\";"
                                
                                when "next-server"; myhost_line << "\tnext-server #{hv.gsub('\"','')};"
                                when "server-name"; 
                                    if is_ip(hv) || is_fqdn(hv)
                                        myhost_line << "\tserver-name \"#{hv.gsub('\"','')}\";"
                                    end
                                else;
                                    next
                            end
                        }
                        if i_am_active != "no"
                            dhout.puts "\n# Host: #{h}"
                            dhout.puts "host #{h} {"
                            myhost_line.each {|myline|
                                dhout.puts myline
                            }
                            dhout.puts "}\n"
                            
                        end
                    end
                }
            end
            if (mfw && mfw.downcase=="yes" ) || (!mfw && (fw && fw.downcase != "no"))
                #retary << "\tAdding automatic dhcp firewall rules for zone: #{z}\n"
                `#{$iptables} -D INPUT -i #{myint} -p tcp --dport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I INPUT -i #{myint} -p tcp --dport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING INPUT ALLOW for tcp port 67 on interface #{myint}\m"
                end
                `#{$iptables} -D INPUT -i #{myint} -p udp --dport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I INPUT -i #{myint} -p udp --dport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING INPUT ALLOW for udp port 67 on interface #{myint}\m"
                end
                `#{$iptables} -D OUTPUT -o #{myint} -p tcp --sport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I OUTPUT -o #{myint} -p tcp --sport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING OUTPUT ALLOW for tcp port 67 responses on interface #{myint}\n"
                end
                `#{$iptables} -D OUTPUT -o #{myint} -p udp --sport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I OUTPUT -o #{myint} -p udp --sport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING OUTPUT ALLOW for udp port 67 responses on interface #{myint}\n"
                end
            end
            ints << myint 
        }
        dhout.close
    if ints
        retary << "Starting DHCPD service\n"
        # start daemon
        retary << `#{$dhcpd} -q #{ints.join(" ")}` 
    end
    return retary
end

def dhcp_allow
    # check to be sure dhcp is active
    if !active?("/jail/dhcp")
        return ""
    end
    ints=Array.new
    retary=Array.new
    gcfg,dl,ml,fw,active,bc,rg,rt=parse_confopts(MFreadlines("/jail/dhcp/options"))
    if !dl
        gcfg << "default-lease-time 14400;"
    end
    if !ml
        gcfg << "max-lease-time 28000;"
    end
    
    # ok we have our header, do our per zone stuff
    zones=Dir.new("/jail/acl/zones")
    zones.each {|z|    
            next if ! test(?r, "/jail/acl/zones/#{z}/interface")
            next if z.include?("\.")
            mycfg=Array.new
            mycfg,mdl,mml,mfw,mactive,mbc,mrg,mrt=parse_confopts(MFreadlines("/jail/acl/zones/#{z}/interface/dhcp"))
            next if !mycfg || mactive.downcase == "no"
            itmp=MFreadlines("/jail/acl/zones/#{z}/interface/int")
            next if !itmp
            myint=itmp.shift
            iptmp=MFreadlines("/jail/acl/zones/#{z}/interface/ip")
            next if !iptmp
            ip=iptmp.shift
            next if !is_ip(ip)
            nmtmp=MFreadlines("/jail/acl/zones/#{z}/interface/netmask")
            next if !nmtmp
            nm=nmtmp.shift
            next if !is_ip(nm)
            if (mfw && mfw.downcase=="yes" ) || (!mfw && (fw && fw.downcase != "no"))
                #retary << "\tAdding automatic dhcp firewall rules for zone: #{z}\n"
                `#{$iptables} -D INPUT -i #{myint} -p tcp --dport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I INPUT -i #{myint} -p tcp --dport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING INPUT ALLOW for tcp port 67 on interface #{myint}\m"
                end
                `#{$iptables} -D INPUT -i #{myint} -p udp --dport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I INPUT -i #{myint} -p udp --dport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING INPUT ALLOW for udp port 67 on interface #{myint}\m"
                end
                `#{$iptables} -D OUTPUT -o #{myint} -p tcp --sport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I OUTPUT -o #{myint} -p tcp --sport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING OUTPUT ALLOW for tcp port 67 responses on interface #{myint}\n"
                end
                `#{$iptables} -D OUTPUT -o #{myint} -p udp --sport 67 -j ACCEPT 2>&1 >> /dev/null`
                if !`#{$iptables} -I OUTPUT -o #{myint} -p udp --sport 67 -j ACCEPT`
                    retary << "\tFAILED ADDING OUTPUT ALLOW for udp port 67 responses on interface #{myint}\n"
                end
            end
            ints << myint 
        }
    return retary
end

def is_ip(ip)
    return nil if ! ip
    tmp=ip.split("\.")
    if (tmp[0] && tmp[0].to_i <= 255) &&
    (tmp[1] && tmp[1].to_i <= 255) &&
    (tmp[2] && tmp[2].to_i <= 255) &&
    (tmp[3] && tmp[3].to_i <= 255)
        return "YES"
    else
        return nil
end

end

def is_fqdn(fqdn)
    if fqdn.index(/[a-zA-Z0-9]\.[a-zA-Z0-9]/)
        return "YES"
    else
        return nil
    end
end

def parse_servers(input)
    t=input.split(/[ ]*,[ ]*|,|[ ]/) 
    srvlist=Array.new
    t.each {|s|
        if is_ip(s)
            srvlist << s
        elsif is_fqdn(s)
            srvlist << s
        end
    }
    if srvlist[1]
        return srvlist.join(", ")
    elsif srvlist[0] !=""
        return srvlist.to_s
    else 
        return nil
    end
end
def parse_confopts(input)
    if !input
        return nil
    end
    tmp=Array.new
    dl=ml=fw=bc=rg=rtr=nil
    active="yes"
    input.each {|line|
        next if !line.index("=")
        k,v=line.split("=")
        key=k.downcase
        if key.index(" ")
            key.gsub!(" ","")
        end
        case key
        when "active";
            active=v.downcase
        when "range";
            vlc=v.downcase
            # we need to make sure that either we have ip ip or dynamic-bootp ip ip
            if vlc.index("dynamic-bootp ")
                # strip that so we can check the other two items for valid ips
                tmp=vlc.gsub("dynamic-bootp ","")
                a,b=tmp.split(" ")
            else
                a,b=vlc.split(" ")
            end
            if is_ip(a) && is_ip(b)
                # ok, we have a valid range line
                tmp << "range #{vlc};"
                rg=vlc
            end
        when "default-lease-time";
            if !v.index(/^0-9/)
                tmp << "default-lease-time #{v};"
                dl=v
            end
        when "max-lease-time";
            if !v.index(/^0-9/)
                tmp << "max-lease-time #{v};"
                ml=v
            end
        when "autofwrule"
            fw=v.downcase
        when "domain-name";
            if v.index("\"")
                tmp << "option domain-name \"#{v.gsub('\"','')}\";"
            else
                tmp << "option domain-name \"#{v}\";"
            end
        when "subnet-mask";
            if is_ip(v)
                tmp << "option subnet-mask #{v};"
            end
        when "domain-name-servers" 
            s=parse_servers(v)
            if s && s!= ""
                tmp << "option domain-name-servers #{s};"
            end
        when "routers"
            s=parse_servers(v)
            if s
                tmp << "option routers #{s};"
                rtr =s
            end
        when "broadcast-address"
            if is_ip(v)
                tmp << "option broadcast-address #{v};"
                bc=v
            end
        when "next-server"
            s=parse_servers(v)
            if s
                tmp << "next-server #{s};"
                rtr =s
            end
        when "host-name"
            tmp << "option host-name #{v};"
        when "boot-size"
            tmp << "option boot-size #{v};"
        when "merit-dump"
            tmp << "option merit-dump #{v};"
        when "swap-server"
            tmp << "option swap-server #{v};"
        when "root-path"
            tmp << "option root-path #{v};"
        when "arp-cache-timeout"
            tmp << "option arp-cache-timeout #{v};"
        when "ntp-servers"
            s=parse_servers(v)
            if s
                tmp << "option ntp-servers #{s};"
            end
        when "netbios-name-servers"
            s=parse_servers(v)
            if s
                tmp << "option netbios-name-servers #{s};"
            end            
        when "font-servers"
            s=parse_servers(v)
            if s
                tmp << "option font-servers #{s};"
            end
        when "tftp-server-name"
            s=parse_servers(v)
            if s
                tmp << "option tftp-server-name \"#{s}\";"
            end
        when "bootfile-name"
            tmp << "option bootfile-name #{v};"
        when "time-offset"
            tmp << "option time-offset #{v};"
        when "time-servers"
            s=parse_servers(v)
            if s
                tmp << "option time-servers #{s};"
            end
        when "log-servers"
            s=parse_servers(v)
            if s
                tmp << "option log-servers #{s};"
            end
        when "cookie-servers"
            s=parse_servers(v)
            if s
                tmp << "option cookie-servers #{s};"
            end
        when "lpr-servers"
            s=parse_servers(v)
            if s
                tmp << "option lpr-servers #{s};"
            end
        when "impress-servers"
            s=parse_servers(v)
            if s
                tmp << "option impress-servers #{s};"
            end
        when "resource-location-servers"
            s=parse_servers(v)
            if s
                tmp << "option resource-location-servers #{s};"
            end
        when "ping-check"
            tmp << "ping-check #{v};"
        when "ping-timeout"
            tmp << "ping-timeout #{v};"
        else
            next
        end
    }
    return tmp,dl,ml,fw,active,bc,rg,rtr
end
def dhcp_restart
    tmp=Array.new
    tmp << dhcp_stop
    tmp << dhcp_start
    return tmp
end
